package org.refresh.injectors;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.refresh.injectors.xml.InjectionException;
import org.refresh.injectors.xml.InvalidInjectionException;
import org.refresh.injectors.xml.XMLAnnotationHandler;
import org.refresh.util.ParsingUtil;

/*******************************************************************************
 * Copyright (c) 2005, 2006 Jules White. All rights reserved. This program and
 * the accompanying materials are made available under the terms of the Eclipse
 * Public License v1.0 which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors: Jules White - initial API and implementation
 ******************************************************************************/
public class XMLInjector {
	
	public class UnknownAnnotationTypeException extends RuntimeException {

		public UnknownAnnotationTypeException(String arg0) {
			super(arg0);
		}

	}

	public static final String FEATURE_KEY = "feature";

	public static final String WITH_KEY = "with";

	public static final int FEATURE_GROUP = 1;

	public static final int ANNOTATION_TYPE_GROUP = 3;

	public static final int ARGS_GROUP = 4;

	public static final String HEAD_REGEX = "\\<\\!\\-\\-[\\s]*feature\\[([\\w]*)\\][\\s]*(\\(([a-zA-Z0-9\\-\\_\\#]*)[\\s]*(\\[[^\\]]*\\])?[\\s]*\\))?[\\s]*\\{[\\s]*(\\-\\-\\>)?";

	public static final String FEATURE_REGEX = "\\<\\-\\-[\\s]*feature\\[()]";

	public static final String HEAD_TERMINATOR = "-->";

	public static final String TAIL_PATTERN_PREFIX = "<!--}";

	public static final String TAIL_PATTERN_POSTFIX = "-->";

	public static final String TAIL_PATTERN_SELF_TERMINATING = "}-->";

	public static final String TAIL_TEMPLATE_REGEX = "\\}[\\s]*with[\\s]*\\{([^\\}]*)\\}\\-\\-\\>";

	public static final Pattern HEAD_PATTERN = Pattern.compile(HEAD_REGEX);

	public static final Pattern TAIL_PATTERN = Pattern
			.compile(TAIL_TEMPLATE_REGEX);

	public static final int WITH_GROUP = 1;

	private boolean enableTracing_ = true;

	private Map<String, Map<String, List<String>>> bindingsTraceInformation_;

	private Map<String, XMLAnnotationHandler> handlerMap_ = new HashMap<String, XMLAnnotationHandler>(
			19);

	public String inject(Map<String, Boolean> features,
			Map<String, Map> values, String conf) {
		
		if(bindingsTraceInformation_ != null)
			bindingsTraceInformation_.clear();
		
		if(enableTracing_)
			buildTraceMap(features, values);
		
		values = processInjections(features, values);
		
		StringBuffer injected = new StringBuffer(conf.length());
		int last = 0;

		Matcher m = HEAD_PATTERN.matcher(conf);

		while (m.find()) {
			int start = m.start();
			int end = m.end();

			String featurename = m.group(FEATURE_GROUP);
			String annotationtype = m.group(ANNOTATION_TYPE_GROUP);
			String args = m.group(ARGS_GROUP);
			String head = conf.substring(start, end);

			if (start != last) {
				injected.append(conf.substring(last, start));
			}

			String with = null;
			String tail = null;
			String template = null;
			int estart = -1;
			if (head.endsWith(HEAD_TERMINATOR)) {
				estart = conf.indexOf(TAIL_PATTERN_PREFIX);
				int eend = conf.indexOf(TAIL_PATTERN_POSTFIX, estart
						+ TAIL_PATTERN_PREFIX.length());
				last = eend + TAIL_PATTERN_POSTFIX.length();
				template = conf.substring(end, estart);
				tail = conf.substring(estart, last);
				Matcher tm = TAIL_PATTERN.matcher(tail);
				if (tm.find()) {
					with = tm.group(WITH_GROUP);
				}
			} else {
				estart = conf.indexOf(TAIL_PATTERN_SELF_TERMINATING, end);
				last = estart + TAIL_PATTERN_SELF_TERMINATING.length();
				head = conf.substring(start, last);
				int tend = conf.lastIndexOf("}", last);
				template = conf.substring(end, tend);
			}

			Boolean enabledval = features.get(featurename);
			boolean enabled = (enabledval != null && enabledval.booleanValue()) ? true
					: false;

			Map bindings = values.get(featurename);

			try {
				if (getEnableTracing()) {
					injected.append(getBindingsTrace(featurename, head, tail,
							template, bindings));
				}
				String processed = process(annotationtype, template,
						featurename, args, bindings, with, enabled);
				injected.append(processed);

			} catch (RuntimeException e) {
				InjectionException ex = new InjectionException(e);
				ex.setHead(head);
				ex.setTail(tail);
				ex.setTailStart(estart);
				ex.setHeadStart(start);
				ex.setTemplate(template);
				ex.setFeature(featurename);
				ex.setBindings(bindings);
				throw ex;
			}
		}

		if (last != conf.length()) {
			injected.append(conf.substring(last, conf.length()));
		}

		return injected.toString();
	}

	public void buildTraceMap(Map<String, Boolean> features,
			Map<String, Map> values) {
		bindingsTraceInformation_ = new HashMap<String, Map<String,List<String>>>();
		for(Object key : features.keySet()){
			if(features.get(key).booleanValue()){
				Map<String,Object> bindings = values.get(key);
				for(String var : bindings.keySet()){
					addTrace(key,var,bindings.get(var));
				}
			}
		}
	}
	
	public Map<String,Map> processInjections(Map<String, Boolean> features,
			Map<String, Map> values) {
		Map<String,Map> ivals = new HashMap<String, Map>();
		for(Object key : features.keySet()){
			if(features.get(key).booleanValue()){
				Map<String,Object> bindings = values.get(key);
				for(String var : bindings.keySet()){
					String[] resolved = resolve(""+key, var);
					String tfeat = resolved[0];
					String tvar = resolved[1];
					Map fvalues = ivals.get(tfeat);
					if(fvalues == null){
						fvalues = new HashMap<String, Map>();
						ivals.put(tfeat,fvalues);
					}
					Object eval = fvalues.get(tvar);
					if(eval != null && !(eval instanceof List)){
						throw new InvalidInjectionException("The variable \""+var+"\" in context \""+key+"\" is not a list and and already has a value set.");
					}
					
					Object dval = bindings.get(var);
					if(eval != null){
						List lval = (List)eval;
						lval.add(dval);
						dval = lval;
					}
					fvalues.put(tvar,dval);
				}
			}
		}
		return ivals;
	}
	
	public void addTrace(Object feature, String var, Object val){
		String[] resolved = resolve(""+feature, var);
		String trgfeat = resolved[0];
		String varname = resolved[1];
		Map<String,List<String>> trace = bindingsTraceInformation_.get(trgfeat);
		if(trace == null){
			trace = new HashMap<String, List<String>>();
			bindingsTraceInformation_.put(trgfeat,trace);
		}
		List<String> binders = trace.get(var);
		if(binders == null){
			binders = new ArrayList<String>();
			trace.put(varname,binders);
		}
		if(val instanceof List){
			List vs = (List)val;
			for(Object o : vs)
				binders.add(""+feature);
		}
		else{
			binders.add(""+feature);
		}
	}
	
	public String[] resolve(String context, String var){
		int first = var.indexOf(".");
		String[] resolved = new String[2];
		if(first > 0){
			resolved[0] = var.substring(0,first);
			resolved[1] = var.substring(first+1);
		}
		else{
			resolved[0] = context;
			resolved[1] = var;
		}
		return resolved;
	}

	public String getBindingsTrace(String feature, String head, String tail,
			String template, Map bindings) {
		String trace = "\n<!--\nThis configuration binding was produced by the annotation:\n##########################################################\n";
		trace += "  required feature:" + feature + "\n";
		trace += "  head:" + stripComments(head) + "\n";
		if (tail != null)
			trace += "  tail:" + stripComments(tail) + "\n";
		if (template != null)
			trace += "  template:" + stripComments(template) + "\n";

		if (bindingsTraceInformation_ != null) {
			trace += "\n  bindings:\n";
			Map<String, List<String>> producermap = bindingsTraceInformation_
					.get(feature);
			if (producermap != null) {
				for (Object key : bindings.keySet()) {
					Object val = bindings.get(key);
					List binders = producermap.get("" + key);
					if (binders != null && binders.size() > 0) {
						if (val instanceof List) {
							List vals = (List) val;
							trace += "    key:" + key
									+ "is a bound to the list:[";
							for (int i = 0; i < vals.size(); i++) {
								Object v = vals.get(i);
								Object binder = binders.get(i);
								trace += "\n      item:\"" + v
										+ "\" bound by the feature(s):"
										+ binder;
							}
							trace += "]\n";
						} else {
							trace += "    key:" + key
									+ " bound by the feature(s):"
									+ toBinderString(binders) + " to:\"" + val
									+ "\"\n";
						}
					}
				}
			}
		}
		trace += "##########################################################\n-->\n";
		return trace;
	}

	public String stripComments(String cstr) {
		return cstr.replaceAll("\\<\\!-\\-", "").replaceAll("\\-\\-\\>", "");
	}

	protected String toBinderString(List binders) {
		String str = "[";
		for (Object b : binders) {
			str += b + ",";
		}
		if (str.endsWith(","))
			str = str.substring(0, str.length() - 1);
		str += "]";
		return str;
	}

	/**
	 * @return the bindingsTraceInformation
	 */
	public Map<String, Map<String, List<String>>> getBindingsTraceInformation() {
		return bindingsTraceInformation_;
	}

	/**
	 * @param bindingsTraceInformation
	 *            the bindingsTraceInformation to set
	 */
	public void setBindingsTraceInformation(
			Map<String, Map<String, List<String>>> bindingsTraceInformation) {
		bindingsTraceInformation_ = bindingsTraceInformation;
	}

	public String process(String annotationtype, String template,
			String feature, String argstr, Map bindings, String withstmt,
			boolean enabled) {
		Map args = getArgs(argstr);
		args.put(FEATURE_KEY, feature);
		args.put(WITH_KEY, withstmt);
		XMLAnnotationHandler handler = getHandler(annotationtype);
		String value = "";
		if (handler == null) {
			throw new UnknownAnnotationTypeException(
					"Could not find an org.refresh.injectors.XMLAnnotationHandler to handle the annotation type \""
							+ annotationtype
							+ "\". You probably made a typo in your annotation name or you haven't installed the correct annotation into the org.refresh.injectors.XMLInjector or you installed your XMLAnnotationHandler under the wrong name.");
		}
		if (enabled) {
			value = handler.handle(template, args, bindings);
		}

		return value;
	}

	public Map getArgs(String argstr) {
		if (argstr == null)
			return new HashMap();

		return ParsingUtil.parseSimpleMap(argstr, "]");
	}

	public void addAnnotationHandler(XMLAnnotationHandler h) {
		handlerMap_.put(h.getType(), h);
	}

	public void removeAnnotationHandler(XMLAnnotationHandler h) {
		handlerMap_.remove(h.getType());
	}

	/**
	 * @return the enableTracing
	 */
	public boolean getEnableTracing() {
		return enableTracing_;
	}

	/**
	 * @param enableTracing
	 *            the enableTracing to set
	 */
	public void setEnableTracing(boolean enableTracing) {
		enableTracing_ = enableTracing;
	}

	public XMLAnnotationHandler getHandler(String annotationtype) {
		return handlerMap_.get(annotationtype);
	}
}
